Gitlab CI 更新遠端伺服器網頁

前言

對於 Gtilab、git 指令或者寫 script 其實都並不是很熟,這次花了一些時間進行部署,希望能寫下來當作學習筆記,也方便未來複習。

想解決的問題是:在 Gitlab 上進行開發,當開發到一個新的版本後,就需要使用 sphinx 將編寫的程式碼及使用文件轉換成 HTML 格式,並將轉換完成的網頁更新到 nginx server上,最後重啟 nginx 的 docker。然而每次都需要人工進行這項操作,覺得實在太不自動化,因此想借助 CI 完成這個流程。

這裡用docker架設的 Gitlab server 和 nginx server 是在不同的兩台機器運作的喔!

關於 Gitlab CI 的概念

開始使用前,少不了需要研究整個運作架構。

Gitlab CI & Gitlab Runner

Gitlab CI 由 Gitlab 自帶,是能進行持續集成的系統,我們需要在 Gitlab 專案裡放置 .gitlab-ci.yml,Gitlab CI 會對裡面的腳本(script)進行解析,並且調用 Gitlab Runner 來運作腳本裡的內容。

簡單來說,Gitlab CI 是管理者,用來管理每個項目的構建狀態。

而建構任務會耗費系統資源,Gitlab CI 本身就是 Gitlab 的一部份,為了不使 Gitlab 性能下降,通常建議整個建構的任務,交由 Runner 來執行,並且使 Runner 運作於與 Gitlab server 不同的機台上。

.gitlab-ci.yml 裡面則編寫不同階段和執行規則,由 Gitlab CI 來進行解讀。

Pipeline

一次Pipeline等於一次的構建任務,裡面可以包含不同階段,例如運行測試、進行佈署等等。當進行git push後,則可以觸發(trigger) Pipeline。

Stages

Stages 便是剛剛提到在 Pipeline 中,不同的任務階段,我們可以在 Pipeline 中去定義這些 Stages,並且 Stages 會有以下的特點:

  • 所有 Stages 會按照指定的順序依序運行
  • 任一個 Stage 只要運行失敗,則該構建任務 (Pipeline) 當即停止,並且該構建任務;反之,全部 Stages 運行成功,則 Pipeline 成功。

Jobs

Jobs 表示在 Stage 裡面執行的工作。在 Stages 內可以有複數個 Jobs,並且會有以下的特點:

  • 相同 Stage 的 Job 會同時執行
  • 任一個 Job 只要運行失敗,則該 Stage 失敗,連動 Pipeline 失敗;反之全部成功,則 Pipeline 成功。

上述內容的參考資料在附註1,裡面有附圖,個人很喜歡這篇簡潔清楚的介紹。

Gitlab CI 建置流程

建置 Gitlab Runner

前述提過,真正要執行腳本還得依靠 Runner,因此我們選擇在架設 nginx 伺服器的這台機器 (系統環境 ubuntu 18.04) 上,將 Runner 給架設起來。

Step 1. 安裝 Gitlab Runner

下載 Gitlab Runner 並裝到指定目錄去:

1
2
wget -O /usr/local/bin/gitlab-runner
https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

給 Gitlab Runner 添加執行權限:

1
sudo chmod +x /usr/local/bin/gitlab-runner

創建 Gitlab Runner 的帳號:

1
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

安裝並啟用服務:

1
2
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

Step 2. 註冊 Gitlab Runner

運行以下命令並開始註冊:

1
sudo gitlab-runner register

待會註冊時使用的 token 可以在 Gitlab 介面的 Setting -> CI/CD 裡面找到。

  • Specific Runner - 該 Runner 指定具體某個專案才可使用
  • Shared Runner - 該 Runner 所有專案都可以使用 (Overview -> CI/CD 才會看到Shared Runner的token)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )
https://xxxxxxxxx/

# 根據需求是要Specific Runner或Shared Runner,填入對應的token
Please enter the gitlab-ci token for this runner
xxxxxxx

# 輸入Runner的描述,後續可在GitLab的UI變更內容
Please enter the gitlab-ci description for this runner
[hostame]: the-description-you-like

# 設定tags,後續可在GitLab變更,未來編寫腳本會需要使用到這個tag
Please enter the gitlab-ci tags for this runner (comma separated):
your-setting-tag

# 選擇Runner的執行者(executor)
Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell:
docker

# 上個步驟選用docker才會有這一步
Please enter the Docker image (eg. ruby:2.1):
alpine:latest

大概需要注意的是,安裝 Gitlab Runner 的這台機器也需要安裝 Git,否則後續運作會有問題,另外最後的 Docker image 不知道填什麼,就填 alpine 吧,因為 alpine 是 CI 默認的部署 docker image。

整個註冊流程完成後,可以在 Gitlab UI 看到下圖中紅色方框圈起來的部分,若註冊的是 Specific Runners,那麼會產生左邊的方框內容。詳情可對照下圖。

撰寫 .gitlab-ci.yml

撰寫這份文件想讓 gitlab CI 進行的事情其實很簡單,大概的思路便是將整段流程拆成兩個部分,第一部分是撰寫 .gitlab-ci.yml 裡的腳本,腳本會先建立連線至 nignx server 那台,並且將 gitlab 專案的內容 scp 傳送到 nignx server,並且啟動在 nginx server 上的腳本;第二部分就是完成剛剛提到在 nignx server 上的腳本,這個腳本會去運行後半段的所有事情,也就是執行 nginx server 上的 sphinx,產出 html 後再放置到 nginx server 的目錄下,最後重啟docker。

腳本內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
image: ubuntu 

stages:
- deploy

before_script:
## Install ssh-agent if not already installed, it is required by Docker.
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
## Run ssh-agent (inside the build environment)
- eval $(ssh-agent -s)

- mkdir -p ~/.ssh
- echo -e "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-add ~/.ssh/id_rsa

- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'

deployment_job:
stage: deploy

script:
- echo ${CI_PROJECT_DIR}
- scp -r $CI_PROJECT_DIR/* at@$DEPLOY_SERVER:$LIB_DIR
- echo 'scp finish.'
- ssh at@$DEPLOY_SERVER "cd $ROOT_DIR && ls &&
chmod +x make_html.sh &&
bash make_html.sh"

only:
- feature/sphinx

tags:
- ubuntu-tag

GitLab Docs 裡面其實都有滿詳盡的說明,有關鍵字列表,像是上面的 before_script、stage 等等,他們不能被設置為任務名稱,在撰寫這份 yaml 時記得先去看一下列表內容。

那麼 stages 下放入的就是對應要執行的 stage 順序,而每一個 job (也就是上述的 deplyment_job 下的內容) ,它的 stage 都需要寫上使用者指定的 stage ,only 指定在專案下的哪一個 branch 被 push 時觸發,tags 就是我們前面在註冊時, 給定 runner 的名稱。

那麼大概需要注意的幾件事情是:

  • 撰寫內容時有可能會需要使用到自定義的環境變量 ( GitLab Docs 左側欄位 CI/CD->Environment variables 可查找相關訊息 ),像是上面的 $SSH_PRIVATE_KEY,因為這份 yaml 是放在專案上的,如果是公開給所有人知道,那麼有一些比較私密的內容不想被別人知道則可以用設置環境變量的方式進行。

    Gitlab UI 裡面 (Setting->CI/CD) 呈現如上述。可在裡面填入變量及對應內容,如 IP 等等。

  • 因為需要 ssh 連線,要記得設置公鑰和私鑰。我們要從 Gitlab CI 這裡建立的 container 連線到 nginx server,所以公鑰要記得給到那台 server,而私鑰則保存到我們剛剛提到的自定義環境變量裡面,如下圖。

    那這邊網路上教學文也很多,就不提了。大概就是記得公鑰要放入 authorized_keys ,私鑰則直接複製貼上上述欄位裡即可。

    • 另外有一個小坑,要記得設置私鑰的自定義環境變數時,旁邊的 Protected 要關掉,否則 ssh 是無法正確連線到伺服器的。

      1
      2
      3
      4
      5
      6
      Pseudo-terminal will not be allocated because stdin is not a terminal.
      Warning: Permanently added '{SERVER_IP}' (ECDSA) to the list of known hosts.
      Permission denied, please try again.
      Permission denied, please try again.
      Permission denied (publickey,password).
      ERROR: Job failed: exit code 1
  • 在 script 裡面寫的指令,要注意 image 的裝設的是哪個環境,例如如果有用到 apt-get 這類的,在 alphine 下,這類指令是不能運作的。

  • 另外,我們在 nginx server 的 scipt 裡面需要啟動 conda 環境,但不知道為什麼一直會報錯,下面附圖是在 gitlab CI 執行時報的錯誤,在 conda activate 時會找不到 conda 指令。

    1
    2
    bash: conda: command not found
    ERROR: Job failed: exit code 1

    這部份猜測有可能是連線過去後,用 ssh 的連線方式無法拉到 .bashrc 的內容,所以找不到 conda,解決方式是把環境的路徑 export PATH="/my_route/anaconda3/bin:$PATH" 直接設置在 script 的最上面。

運行過程中,零零總總還是有遇到各式各樣的 bug,像一開始其實是想要在 gitlab CI runner 上直接運行兩份腳本的所有內容,但是發現的問題是我們自己的 package requirement.txt 需要很多套件安裝,sphinx 要產生檔案時也需要時間,整體 runner 到執行結束就會運作很久,因此才想改成後來這個方案。

小結

並且這份文件大概是 Gitlab CI 用好後,又隔了一陣子才寫,所以有些遇到的問題大概也不是記得特別清楚。那麼總之,我對於目前整體的運作理解就做個小總結,其實好像還是有想不太清楚的地方:runner 雖然是架設在 remote server 上,但是由 Gitlab CI 開始指派 runner 運作時,因為我的 runner 的執行者選擇的是 docker,所以會在 remote server上運作一個 container,該 container 下會有我的專案內容,大概是如此。只是我很困惑的點是,明明都是在同一台電腦 (就是那台 remote server) 裡運作 container,那為何不能直接指向伺服器路徑裡的特定檔案就好?還得依靠 ssh 連線到這台電腦執行 script… 總覺得自己好像繞了遠路。

那最後有什麼錯誤或者觀念不理解的地方麻煩賜教。

reference: